4.1 定义
函数是结构化编程的最小模块单元。它将复杂的算法过程分解为若干较小任务,隐藏相关细节,使得程序结构更加清晰,易于维护。函数被设计成相对独立,通过接收输入参数完成一段算法指令,输出或存储相关结果。因此,函数还是代码复用和测试的基本单元。
关键字func用于定义函数。Go中的函数有些不太方便的限制,但也借鉴了动态语言的某些优点。
- 无须前置声明。
- 不支持命名嵌套定义(nested)。
- 不支持同名函数重载(overload)。
- 不支持默认参数。
- 支持不定长变参。
- 支持多返回值。
- 支持命名返回值。
- 支持匿名函数和闭包。
和前面曾说过的一样,左花括号不能另起一行。
func test()
{ // 错误:syntax error:unexpected semicolon or newline before{
}
func test(x int) { // 错误:test redeclared in this block
}
func main() {
func add(x,y int)int{ // 错误:syntax error:unexpected add,expecting(
return x+y
}
}函数属于第一类对象,具备相同签名(参数及返回值列表)的视作同一类型。
func hello() {
println("hello,world!")
}
func exec(f func()) {
f()
}
func main() {
f:=hello
exec(f)
}第一类对象(first-class object)指可在运行期创建,可用作函数参数或返回值,可存入变量的实体。最常见的用法就是匿名函数。
从阅读和代码维护的角度来说,使用命名类型更加方便。
// 定义函数类型
type FormatFunc func(string, ...interface{}) (string,error)
// 如不使用命名类型,这个参数签名会长到没法看
func format(f FormatFunc,s string,a...interface{}) (string,error) {
return f(s,a...)
}函数只能判断其是否为nil,不支持其他比较操作。
func a() {}
func b() {}
func main() {
println(a==nil)
println(a==b) // 无效操作:a==b(func can only be compared to nil)
}
从函数返回局部变量指针是安全的,编译器会通过逃逸分析(escape analysis)来决定是否在堆上分配内存。
func test() *int{
a:=0x100
return &a
}
func main() {
var a*int=test()
println(a, *a)
}输出:
$go build-gcflags"-l-m" // 禁用函数内联,输出优化信息
moved to heap:a
&a escapes to heap
$go tool objdump-s"main\.main"test// 反汇编确认
TEXT main.main(SB)test.go
CALL main.test(SB)
$ ./test
0xc820074000 256
函数内联(inline)对内存分配有一定的影响。如果上例中允许内联,那么就会直接在栈上分配内存。
$go build-gcflags"-m" // 默认优化方式,允许内联
inlining call to test
main&a does not escape
$go tool objdump-s"main\.main"test
TEXT main.main(SB)test.go
MOVQ$0x100,0x10(SP)
LEAQ 0x10(SP),BX
MOVQ BX,0x18(SP)
MOVQ 0x18(SP),BX
MOVQ BX,0(SP)
CALL runtime.printpointer(SB)
当前编译器并未实现尾递归优化(tail-call optimization)。尽管Go执行栈的上限是GB规模,轻易不会出现堆栈溢出(stack overflow)错误,但依然需要注意拷贝栈的复制成本。
内存管理相关内容,请阅读本书下卷“源码剖析”。
建议命名规则
在避免冲突的情况下,函数命名要本着精简短小、望文知意的原则。
- 通常是动词和介词加上名词,例如scanWords。
- 避免不必要的缩写,printError要比printErr更好一些。
- 避免使用类型关键字,比如buildUserStruct看上去会很别扭。
- 避免歧义,不能有多种用途的解释造成误解。
- 避免只能通过大小写区分的同名函数。
- 避免与内置函数同名,这会导致误用。
- 避免使用数字,除非是特定专有名词,例如UTF8。
- 避免添加作用域提示前缀。
- 统一使用camel/pascal case拼写风格。
- 使用相同术语,保持一致性。
- 使用习惯用语,比如init表示初始化,is/has返回布尔值结果。
- 使用反义词组命名行为相反的函数,比如get/set、min/max等。
函数和方法的命名规则稍有些不同。方法通过选择符调用,且具备状态上下文,可使用更简短的动词命名。